package com.wayne.binding;

import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ObjectUtil;
import com.wayne.pojo.Configuration;
import com.wayne.pojo.MappedStatement;
import com.wayne.pojo.SqlCommandType;
import com.wayne.sqlSession.SqlSession;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(mapperInterface, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 判断mapper 中的方法类型，最终调用的还是sqlSession中的方法
    switch (command.getType()) {
      case INSERT: {
        Object parameter = getNamedParams(args, method.getParameterNames());
        // 执行INSERT操作 && 转换rowCount
        result = rowCountResult(sqlSession.insert(command.getName(), parameter));
        break;
      }
      case UPDATE: {
        // 转参操作
        Object parameter = getNamedParams(args, method.getParameterNames());
        // 执行UPDATE操作 && 转换rowCount
        result = rowCountResult(sqlSession.update(command.getName(), parameter));
        break;
      }
      case DELETE: {
        // 转参操作
        Object parameter = getNamedParams(args, method.getParameterNames());
        // 执行DELETE操作 && 转换rowCount
        result = rowCountResult(sqlSession.delete(command.getName(), parameter));
        break;
      }
      case SELECT:
        Object parameter = getNamedParams(args, method.getParameterNames());
        // 获取被调用方法的返回值类型
        if (method.isReturnsMany()) {
          return sqlSession.selectList(command.getName(), parameter);
        }
        return sqlSession.selectOne(command.getName(), parameter);
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    // 返回结果
    return result;
  }

  private Object rowCountResult(int rowCount) {
    final Object result;
    if (method.isReturnsVoid()) {
      result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      result = (long) rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      result = rowCount > 0;
    } else {
      throw new BindingException(
          "Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }

  public Object getNamedParams(Object[] args, LinkedList<String> parameterNames) {
    if (args == null) {
      return null;
    }
    final Map<String, Object> param = new ParamMap<>();
    if (parameterNames.size() == 1) {
      if( !ObjectUtil.isBasicType(args[0]) && !ClassUtil.isPrimitiveWrapper(args[0].getClass())){
        return args[0];
      } else {
        param.put(parameterNames.get(0), args[0]);
      }
    } else {
      for (int i = 0; i < parameterNames.size(); i++) {
        param.put(parameterNames.get(i), args[i]);
      }
    }
    return param;
  }

  public static class ParamMap<V> extends HashMap<String, V> {

    private static final long serialVersionUID = -2212268410512043556L;

    @Override
    public V get(Object key) {
      if (!super.containsKey(key)) {
        throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
      }
      return super.get(key);
    }

  }

  public static class SqlCommand {

    private final String name;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
      if (ms == null) {
        name = null;
        type = SqlCommandType.FLUSH;
      } else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

    public String getName() {
      return name;
    }

    public SqlCommandType getType() {
      return type;
    }

    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass,
                                                   Configuration configuration) {
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      }
      if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
  }

  public static class MethodSignature {
    private final boolean returnsVoid;
    private final boolean returnsMany;
    private final Class<?> returnType;
    private final Class<?>[] parameterTypes;
    private final Parameter[] parameters;
    private final LinkedList<String> parameterNames;

    public MethodSignature(Class<?> mapperInterface, Method method) {
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = Collection.class.isAssignableFrom(this.returnType) || this.returnType.isArray();
      this.parameterTypes = method.getParameterTypes();
      this.parameters = method.getParameters();
      this.parameterNames = new LinkedList<>();
      if (this.parameters.length > 0) {
        for (Parameter parameter : this.parameters) {
          parameterNames.add(parameter.getName());
        }
      }
    }

    public Class<?> getReturnType() {
      return returnType;
    }

    public boolean isReturnsVoid() {
      return returnsVoid;
    }

    public Class<?>[] getParameterTypes() {
      return parameterTypes;
    }

    public LinkedList<String> getParameterNames() {
      return parameterNames;
    }

    public boolean isReturnsMany() {
      return returnsMany;
    }
  }
}